/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE */

package au.com.lastweekend.ws2300;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Extends the raw protocol to allow for the flakiness of the actual device and for the limited capability of the protocol in terms
 * of numbers of reads and writes.
 * <p>
 * Each IO to the underlying device will be tried {@link #getMaxRetries} times before throwing an IOException.
 * 
 * @author ggardner
 * 
 */
public class SafeDevice extends RawDevice {

    private static final Logger LOG = LoggerFactory.getLogger(SafeDevice.class);

    abstract class Retriable<T> {

        private String name;

        public Retriable(String name) {

            this.name = name;
        }

        public T doRetry() throws IOException {

            int retries = 0;
            IOException ex = null;
            while (retries < maxRetries) {
                try {
                    if (retries > 0) {
                        Thread.sleep(retryDelay * retries);
                    }
                    return retry();
                } catch (IOException e) {
                    if (LOG.isDebugEnabled()) {
                        // include the stack trace.
                        LOG.warn(String.format("Failed %s at attempt %d", name, retries), e);
                    } else {
                        LOG.warn(String.format("Failed %s at attempt %d due to %s", name, retries, e.getMessage()));
                        ex = e;
                    }
                } catch (InterruptedException e1) {
                    // bail out straight away.
                    throw newIOException(String.format("Interrupted waiting to try %s attempt %d", name, retries), e1);
                }
                retries++;
            }

            throw newIOException(String.format("Failed %s after %d attempts", name, retries), ex);

        }

        protected abstract T retry() throws IOException;

    }

    private int maxRetries = 5;
    private int retryDelay = 200;

    public SafeDevice(InputStream in, OutputStream out) {

        super(in, out);
    }

    public SafeDevice() {

    }

    @Override
    /**
     * As well as retrying this method breaks up the read into chunks of the maximum size supported by the protocol.
     */
    public synchronized byte[] read(int address, int numBytes) throws IOException {

        int bytesRemaining = numBytes;
        int chunkOffset = 0;

        byte[] resultNibbles = new byte[2 * numBytes];

        while (bytesRemaining > 0) {
            final int nextChunk = Math.min(bytesRemaining, MAX_READ_BYTES);
            final int nextAddress = address + chunkOffset;

            Retriable<byte[]> retryRead = new Retriable<byte[]>("read") {

                protected byte[] retry() throws IOException {

                    return superRead(nextAddress, nextChunk);
                }
            };

            byte[] chunkNibbles = retryRead.doRetry();

            System.arraycopy(chunkNibbles, 0, resultNibbles, chunkOffset, nextChunk * 2);

            bytesRemaining = bytesRemaining - nextChunk;
            chunkOffset = chunkOffset + nextChunk * 2;
        }

        return resultNibbles;

    }

    @Override
    /**
     * TODO: Likelihood of IOExceptions increases with the number of nibbles being written. If this becomes a problem we could have
     * rawDevice throw a nice exception to tell us where it was up to so we can do a smarter restart.
     */
    public synchronized void write(final int address, final byte[] nibbles) throws IOException {

        Retriable<Object> retryWrite = new Retriable<Object>("write") {

            @Override
            protected Object retry() throws IOException {

                superWrite(address, nibbles);
                return null;
            }

        };

        retryWrite.doRetry();
    }

    @Override
    public synchronized void writeBit(final int nibbleAddress, final int bitAddress, final boolean setBit) throws IOException {

        Retriable<Object> retryWriteBit = new Retriable<Object>("writeBit") {

            @Override
            protected Object retry() throws IOException {

                superWriteBit(nibbleAddress, bitAddress, setBit);
                return null;
            }
        };

        retryWriteBit.doRetry();
    }

    private byte[] superRead(int address, int numBytes) throws IOException {

        return super.read(address, numBytes);
    }

    private void superWrite(int address, byte[] nibbles) throws IOException {

        super.write(address, nibbles);
    }

    private void superWriteBit(int nibbleAddress, int bitAddress, boolean setBit) throws IOException {

        super.writeBit(nibbleAddress, bitAddress, setBit);
    }

    public int getMaxRetries() {

        return maxRetries;
    }

    /**
     * Number of successive read/write failures before we give up. default 5.
     * 
     * @param maxRetries
     */
    public void setMaxRetries(int maxRetries) {

        if (maxRetries < 1) {
            throw new IllegalArgumentException("Retries must be at least 1");
        }
        this.maxRetries = maxRetries;
    }

    public int getRetryDelay() {

        return retryDelay;
    }

    /**
     * Set milliseconds to sleep between retries. default 200ms
     * 
     * The delay increases by this amount for successive failures.
     * 
     * @param retryDelay
     */
    public void setRetryDelay(int retryDelay) {

        if (retryDelay <= 0) {
            throw new IllegalArgumentException("sleepBasis must be greater than 0");
        }

        this.retryDelay = retryDelay;
    }

}